package com.atguigu.gmall.item.service.impl;


import com.atguigu.gmall.common.constant.RedisConstant;
import com.atguigu.gmall.feign.list.GoodsFeignClient;
import com.atguigu.gmall.feign.product.CategoryFeignClient;
import com.atguigu.gmall.feign.product.SaleAttrFeignClient;
import com.atguigu.gmall.feign.product.SkuInfoFeignClient;
import com.atguigu.gmall.item.component.RedisDisLockService;
import com.atguigu.gmall.item.service.ItemService;
import com.atguigu.gmall.model.product.BaseCategoryView;
import com.atguigu.gmall.model.product.SkuImage;
import com.atguigu.gmall.model.product.SkuInfo;
import com.atguigu.gmall.model.product.SpuSaleAttr;
import com.atguigu.gmall.cache.annotation.GmallCache;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class ItemServiceImpl implements ItemService {


    @Autowired
    SkuInfoFeignClient skuInfoFeignClient;

    @Autowired
    CategoryFeignClient categoryFeignClient;


    @Autowired
    SaleAttrFeignClient saleAttrFeignClient;

    ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RBloomFilter<Object> skuIdBloom;  //布隆里面必须有数据库的全量数据

    @Autowired
    ThreadPoolExecutor coreThreadPool;

    Map<Long, Map<String, Object>> cache = new HashMap<>();


    // 秒 分 时  日 月 周

    @Autowired
    RedisDisLockService redisDisLockService;

    @Autowired
    RedissonClient redisson;

    @Autowired
    GoodsFeignClient goodsFeignClient;

//    @Scheduled(cron = "0 30 3 */7 * *")
//    public void initBloom(){
//        //查出所有商品id'
//        //放到布隆中
//    }


    //希望cacheKey是一个动态计算

    //    @GmallCache(cacheKey = "#{#method.getName()+':'+#args[0]}")
    @GmallCache(
            cacheKey = RedisConstant.SKU_DETAIL + "#{#args[0]}",
            bloomName = RedisConstant.SKUIDS_BLOOM,
            bloomValue = "#{new Long(#args[0])}",
            //参数项，可以选择使用本地锁还是分布式锁
            lockKey = RedisConstant.SKU_DETAIL_LOCK + "#{#args[0]}"
    )
    @Override
    public Map<String, Object> getItemDetail(Long skuId) throws Exception {
        log.info("rpc查询商品详情....");
        Map<String, Object> data = getDetailDataFromRpc(skuId);

        return data;
    }



    @Override
    public void incrHotScore(Long skuId) {
        //100w，临时保存每个商品的热度。 100
        //Set:
        BoundZSetOperations<String, String> zSetOps = redisTemplate.boundZSetOps(RedisConstant.SKU_HOTCORE_SET);

        //去ZSet里面拿到 53商品的热度分。+1
        Double score = zSetOps.incrementScore(skuId + "", 1.0);//redis中加一分
        if(score % 100 == 0){
            //es同步一下分数.  skuId==score
            goodsFeignClient.updateGoodsHotScore(skuId,score.longValue());
        }

    }

    //缓存+布隆+锁+数据库
    //AOP + 自定义注解 @MallCache
    //AOP + 自定义注解 @Transaction
    public Map<String, Object> getItemDetailWithRedissonAndBloom(Long skuId) throws Exception {
        RLock lock = redisson.getLock(RedisConstant.SKU_DETAIL_LOCK + skuId);
        //1、先查缓存
        Map<String, Object> cache = getItemDetailFromCache(skuId);
        //2、null、new hashmap()、真数据
        if (cache == null) {
            //3、缓存没命中，回源
            log.info("{}-item-detail 缓存未命中，准备回源", skuId);
            //4、回源之前先问布隆
            if (skuIdBloom.contains(skuId)) { //布隆：【解决：随机值穿透攻击】
                //5、布隆认为有，开始真正回源
                //7、准备分布式锁
                boolean tryLock = false;
                try {
//                    lock.lock(); //阻塞抢锁
                    lock.tryLock(30, 10, TimeUnit.SECONDS);
                    tryLock = lock.tryLock(); //加锁；【解决：缓存击穿问题】
                    if (tryLock) {
                        //8、加锁成功。开始远程查询
                        Map<String, Object> data = getDetailDataFromRpc(skuId);
                        //9、查到的数据保存缓存
                        saveDataToCache(skuId, data); //缓存null值： 自动判断null，并【缓存null值；解决缓存穿透问题】
                        //10、返回结果
                        return data;
                    } else {
                        //11、加锁失败，等待1秒
                        TimeUnit.SECONDS.sleep(1);
                        //12、重新进入缓存的完整逻辑【递归逻辑】
                        return getItemDetail(skuId);
                    }
                } finally {
                    if (tryLock) { //解锁
                        try {
                            lock.unlock();
                        } catch (Exception e) {
                        }
                        ;
                    }
                }
            } else {
                //6、布隆说没有，直接返回null
                return null;
            }
        } else {
            return cache;
        }
    }

    /**
     * 获取商品详情，使用Redisson提供的分布式锁
     *
     * @param skuId
     * @return
     * @throws Exception
     */
    public Map<String, Object> getItemDetailWithRedisson(Long skuId) throws Exception {
        //准备一个锁
        RLock lock = redisson.getLock(RedisConstant.SKU_DETAIL_LOCK + skuId);

        //1、先查缓存
        Map<String, Object> cache = getItemDetailFromCache(skuId); // 777, null;
        //null、【new HashMap<>()（数据库就是没有）;、真map（数据库的真实数据）】
        if (cache == null) {
            //2、缓存没有，【回源】 真正查数据库 （避免回源次数）
            //缓存没命中，回源
            boolean b = false;
            try {
//                lock.lock(); //阻塞加锁，30s，且会自动续期，不担心掉电死锁问题   lock(-1, null, false);
//                lock.lock(30,TimeUnit.SECONDS); //等待加锁，加上以后30s自动解锁
//                lock.tryLock(10,30,TimeUnit.SECONDS);//10s以内尝试疯狂抢锁。抢到以后，给锁设置30s过期时间
                //给了时间，就用户自定义的时间，不给时间就是-1；
                //用户不指定时间；就用这个时间 lockWatchdogTimeout = 30 * 1000; 作为锁的自动释放时间
                //redisson底层利用发布订阅机制进行抢锁；类似JUC中利用 thread.wait()/notify()进行抢锁

                b = lock.tryLock(); //锁默认多久呢？30s，且会自动续期，不担心掉电死锁问题
                //tryAcquireOnceAsync(-1(waittime), -1(leasetime), null, threadId);
                if (b) {
                    Map<String, Object> data = null;
                    try {
                        data = getDetailDataFromRpc(skuId);
                    } catch (Exception e) {
                        //远程出异常没数据
                    }
                    saveDataToCache(skuId, data);
                    return data;
                } else {
                    TimeUnit.SECONDS.sleep(1);  //等待以后重试
                    getItemDetail(skuId);
                }
            } finally {
                if (b) {
                    try {
                        lock.unlock(); //redisson自动判断。如果要解的锁是别人的锁，会抛异常
                    } catch (Exception e) {

                    }
                }
            }
        }
        return cache;
    }

    /**
     * 有锁场景100w并发逻辑
     * 1、查缓存
     * 1.1 有。 直接返回
     * 1.2 没有。（准备查库了）
     * 1.2.1  加分布式锁； 只有一个返回成功
     * 1.2.1.1：  成功
     * 1）、查数据库
     * 2）、放缓存
     * 3）、返回结束
     * 1.2.1.2：  失败
     * 1）、自旋等锁 while(true) 疯狂发抢锁指令
     * 2）、等 1s，直接查缓存。 bug？ 99w靠那一个，可能不可靠？
     * 可靠逻辑？没有。
     * 相对可靠逻辑。等 1s。直接查缓存，缓存没有，继续从1开始的流程； 有限制次数的等待
     *
     * @param skuId
     * @return
     * @throws JsonProcessingException sku/detail/52
     *                                 sku/detail/51
     *                                 sku/detail/50
     */

    public Map<String, Object> getItemDetailUseMyWriteDeLock(Long skuId) throws Exception {

//        Thread thread = Thread.currentThread();
        //1、先查缓存
        Map<String, Object> cache = getItemDetailFromCache(skuId);
        if (cache == null) {
            //缓存没有，远程链路开始查询
            //2、防止百万并发同时判断为null，去查数据库
            boolean lock = false;
            try {
                lock = redisDisLockService.lock("lock:" + skuId);
                if (lock) {
                    //加锁成功，由于停电，导致finally没运行，解锁失败。
                    System.out.println("查库啦......");
                    //redis：30s; 业务不结束，每隔10s，给他续满。redis恢复到30s
                    //定时任务
                    Map<String, Object> data = getDetailDataFromRpc(skuId);

//                    TimeUnit.SECONDS.sleep(60);
                    saveDataToCache(skuId, data);
                    return data;
                } else {
                    //加锁失败，等1s进入递归流程； 如何限制递归重入次数。
                    TimeUnit.SECONDS.sleep(1);
                    return getItemDetail(skuId); //
                }
            } finally {
                if (lock) { //有锁解锁
                    redisDisLockService.unlock("lock:" + skuId);
                }
            }
        } else {
            //缓存中有直接返回
            return cache;
        }
    }


    /**
     * 把sku保存到缓存
     * 默认缓存中的所有东西，都应该给过期时间。
     *
     * @param skuId
     * @param data
     */
    private void saveDataToCache(Long skuId, Map<String, Object> data) throws JsonProcessingException {
        String key = RedisConstant.SKU_DETAIL + skuId;
        //判断，如果数据库查的还是null
        if (data == null) {
            //给缓存中放个n，给一个短暂的过期时间；这个时间内不怕被攻击，这个时间一过。再去数据库查一次（加锁了）
            redisTemplate.opsForValue().set(key, "n", 30, TimeUnit.MINUTES); //防止 “缓存穿透攻击”；要进行空结果缓存
        } else {
            //保存到缓存；真正数据，缓存时间长一点。3天+随机几秒的值。让保存数据的时间维度是散列的
            redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(data), 3, TimeUnit.DAYS);
        }

    }

    /**
     * 从缓存中查sku
     *
     * @param skuId
     * @return
     * @throws JsonProcessingException
     */
    private Map<String, Object> getItemDetailFromCache(Long skuId) throws JsonProcessingException {
        String key = RedisConstant.SKU_DETAIL + skuId;
        String json = redisTemplate.opsForValue().get(key);
        //从缓存中获取的json有三种情况；
        // null：   代表缓存从来没有存过这个东西，说明这是第一次查。需要回源
        // "n"：    代表缓存以前回源查过这个数据，只不过数据库是真没有。
        // “真json”：代表缓存以前回源查过这个数据，数据库是真有。
        if (StringUtils.isEmpty(json)) {
            //null  代表缓存从来没有存过这个东西，说明这是第一次查。需要回源
            return null;
        } else if ("n".equals(json)) {
            // "n"：    代表缓存以前回源查过这个数据，只不过数据库是真没有。
            return new HashMap<>();
        } else {
            //“真json”：代表缓存以前回源查过这个数据，数据库是真有。
            return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
            });
        }

    }

    //1、查数据，过redis缓存
    public Map<String, Object> getItemDetailFromRedisCache(Long skuId) throws JsonProcessingException {

        ValueOperations<String, String> ops = redisTemplate.opsForValue();

        //查sku用的key
        String key = RedisConstant.SKU_DETAIL + skuId; //sku:detail:53  sku:detail:54 sku:detail:55
        String json = ops.get(key);
        //1、先判断缓存中是否有
        if (StringUtils.isEmpty(json)) {
            //1.1 缓存中没有，调用真正业务进行查询

            Map<String, Object> data = getDetailDataFromRpc(skuId);
            //1.2 给缓存中放一份
            String jsonStr = objectMapper.writeValueAsString(data);
            ops.set(key, jsonStr);
            return data;
        } else {
            //2. 缓存中有 ，转成指定的JavaBean，返回给页面 TypeReference<T>
            Map<String, Object> data = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
            });
            return data;
        }

    }
    //以下是本地缓存代码
//        Map<String, Object> data = cache.get(skuId);
//        if(data == null){
//            Map<String, Object> rpc = getDetailDataFromRpc(skuId);
//            cache.put(skuId,rpc);
//            return rpc;
//        }else {
//            return data;
//        }

    /**
     *
     * 商品详细的真正查询逻辑
     * 有了异步，第一次查询会很快结束
     * @param skuId
     * @return
     * @throws JsonProcessingException
     */
    private Map<String, Object> getDetailDataFromRpc(Long skuId) throws JsonProcessingException {
        Map<String, Object> data = new HashMap<>();
        CompletableFuture<Void> categoryViewFuture = CompletableFuture.runAsync(() -> {
            //准备好以下数据
            //1、categoryView（category1Id、category1Name、category2Id、category2Name、category3Id、category3Name）
            // 代表当前sku 所在的完整分类信息
            BaseCategoryView categoryView = categoryFeignClient.getCategoryPathBySkuId(skuId);
            data.put("categoryView", categoryView);
        }, coreThreadPool);


        CompletableFuture<SkuInfo> skuInfoFuture = CompletableFuture.supplyAsync(() -> {
            //2、skuInfo（skuName、id、skuDefaultImg、skuImageList.....）  代表当前sku的详情，来源于数据库sku_info表
            SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
            data.put("skuInfo", skuInfo);
            return skuInfo;
        }, coreThreadPool);

        //查图片
        CompletableFuture<Void> imagesFuture = skuInfoFuture.thenAcceptAsync(skuInfo -> {
            //2.1、查询当前sku对应的所有图片
            List<SkuImage> images = skuInfoFeignClient.skuImageList(skuId);
            skuInfo.setSkuImageList(images);
        }, coreThreadPool);


        CompletableFuture<Void> priceFuture = CompletableFuture.runAsync(() -> {
            //3、price 单独查一下
            BigDecimal price = skuInfoFeignClient.getSkuPrice(skuId);
            data.put("price", price);
        }, coreThreadPool);


        CompletableFuture<Void> spuSaleAttrListFuture = skuInfoFuture.thenAcceptAsync(skuInfo -> {
            //4、spuSaleAttrList 当前sku对应的spu的所有销售属性值组合; 标记当前sku对应的那种组合
            Long spuId = skuInfo.getSpuId();
            List<SpuSaleAttr> saleAttrValues = saleAttrFeignClient.getSpuSaleAttrBySpuId(skuId, spuId);
            data.put("spuSaleAttrList", saleAttrValues);
        }, coreThreadPool);


        CompletableFuture<Void> valuesSkuJsonFuture = skuInfoFuture.thenAcceptAsync(skuInfo -> {
            //5、valuesSkuJson   包含了每一种销售属性组合对应是哪个skuId。方便浏览器跳转
            //就是一个Map  key: 138|140  value: 53；
            Map<String, Object> zuhe = saleAttrFeignClient.getSpuDeSkuSaleAttrValuesZuhe(skuInfo.getSpuId());
            //前端需要的是字符串。
            String jsonString = null;
            try {
                jsonString = objectMapper.writeValueAsString(zuhe);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            data.put("valuesSkuJson", jsonString);
        }, coreThreadPool);


        CompletableFuture.allOf(categoryViewFuture, imagesFuture,
                priceFuture, spuSaleAttrListFuture, valuesSkuJsonFuture)
                .join(); //所有异步都执行完成以后

        return data;
    }
}
